using System.Reflection;
using System.Collections.Generic;
using System;
using System.ComponentModel;
namespace Gl.SettingsManager
{
	internal class ObjectPropertyManager
	{
		private Dictionary<string, PropertyInfo> _properties = new Dictionary<string, PropertyInfo>();
		private Dictionary<string, object[]> _propertiesAttributes = new Dictionary<string, object[]>();

		private Type _operatableType = null;

		private const BindingFlags FlagsToGetAllDeclaredProperties = BindingFlags.DeclaredOnly | BindingFlags.Instance |
				BindingFlags.Public | BindingFlags.NonPublic;

		public ObjectPropertyManager(Type objType)
		{
			if (objType == null)
				throw new ArgumentNullException("objType");
			_operatableType = objType;
			ProccessType(objType);
		}

		public void InitializeObject(SettingsCollection settings, object obj, bool validate)
		{
			if (obj.GetType() != _operatableType)
				throw new SettingsException(Exceptions.InternalErrorTypesNotEquals);

			foreach (Setting setting in settings)
			{
				if (!_properties.ContainsKey(setting.Name))
					continue;

				TypeConverter convertFrom = GetConverter(_properties[setting.Name].PropertyType);
				object value = convertFrom.ConvertFromInvariantString(setting.Value);

				if (value == null || (value is string && string.IsNullOrEmpty(Convert.ToString(value))))
					value = GetDefault(_properties[setting.Name]);

				if (validate)
					ValidateValue(_properties[setting.Name],ref value);

				_properties[setting.Name].SetValue(obj, value, null);
			}
		}

		public SettingsCollection CollectSettingValues(object obj, bool validate)
		{
			if (obj.GetType() != _operatableType)
				throw new SettingsException(Exceptions.InternalErrorTypesNotEquals);

			SettingsCollection collection = new SettingsCollection(_operatableType.FullName,
				Utils.GetContextName(obj));

			foreach (string propertyName in _properties.Keys)
			{
				object objVal = _properties[propertyName].GetValue(obj, null);

				if (validate)
					ValidateValue(_properties[propertyName], ref objVal);

				TypeConverter converter = GetConverter(_properties[propertyName].PropertyType);

				string serValue = converter.ConvertToInvariantString(objVal);

				collection.Add(propertyName, serValue);
			}

			return collection;
		}

		private void ProccessType(Type objType)
		{
			PropertyInfo[] objectProperties = objType.GetProperties(FlagsToGetAllDeclaredProperties);

			ProccessSchemas(objType);

			foreach (PropertyInfo prop in objectProperties)
			{
				if (Utils.GetAttribute<SettingPropertyAttribute>(prop) != null) {
					List<object> attributes = new List<object>();
					attributes.Add(prop.GetCustomAttributes(typeof(SettingPropertyAttribute), false));
					attributes.AddRange(prop.GetCustomAttributes(typeof(SettingValidatorBaseAttribute), false));
					AddProperty(prop, attributes.ToArray());
				}
			}

			bool proccessBaseType;
			InheritBaseClassSettingsAttribute attr = 
				(InheritBaseClassSettingsAttribute)Utils.GetAttribute<InheritBaseClassSettingsAttribute>(objType);
			proccessBaseType = attr == null ? true : attr.InhertiBaseClassSettings;

			if (proccessBaseType && objType.BaseType != null)
				ProccessType(objType.BaseType);
		}

		private object GetDefault(PropertyInfo property)
		{
			SettingPropertyAttribute propAttribute = null;
			foreach (object obj in _propertiesAttributes[property.Name])
				if (obj is SettingPropertyAttribute)
					propAttribute = obj as SettingPropertyAttribute;

			return propAttribute != null ? propAttribute.DefaultValue : null;
		}

		private void ValidateValue(PropertyInfo property, ref object value)
		{
			foreach (object obj in _propertiesAttributes[property.Name])
			{
				if (obj is SettingValidatorBaseAttribute)
				{
					SettingValidatorBaseAttribute settAttr = (SettingValidatorBaseAttribute)obj;
					try
					{
						settAttr.Validate(ref value);
					}
					catch (SettingsException exp)
					{
						throw new SettingsException(Exceptions.ValidatorFailed,
							property.DeclaringType + ", " + property.Name, exp.Message);
					}
				}
			}
		}

		private void ValidateProperty(PropertyInfo prop)
		{
			if (!Utils.HasGetter(prop))
				throw new SettingsException(Exceptions.PropertyMustHaveGetAccessor, prop.Name);
			if (!Utils.HasSetter(prop))
				throw new SettingsException(Exceptions.PropertyMustHaveSetAccessor, prop.Name);
			if (Utils.IsIndexer(prop))
				throw new SettingsException(Exceptions.IndexersArNotSopported, prop.Name);
		}

		private TypeConverter GetConverter(Type type)
		{
			TypeConverter converter = TypeDescriptor.GetConverter(type);

			if (converter == null)
				throw new SettingsException(Exceptions.ConverterForTypeNotFound, type.FullName);

			return converter;
		}

		private void AddProperty(PropertyInfo prop, object[] propertyAttributes)
		{
			ValidateProperty(prop);
			if (_properties.ContainsKey(prop.Name))
				throw new SettingsException(Exceptions.PropertyDefinedDoubleTimes, prop.Name);
			_properties.Add(prop.Name, prop);
			_propertiesAttributes.Add(prop.Name, propertyAttributes);
		}

		private void ProccessSchemas(Type objType)
		{
			List<PropertyInfo> schemaProperties = new List<PropertyInfo>();

			foreach (SettingsSchemaAttribute schema in Utils.GetAttributes<SettingsSchemaAttribute>(objType))
			{
				PropertyInfo[] schemaProps = schema.SchemaType.GetProperties(FlagsToGetAllDeclaredProperties);

				foreach (PropertyInfo property in schemaProps)
				{
					if (Utils.GetAttribute<SettingPropertyAttribute>(property) == null)
						continue;
					PropertyInfo propertyObj = objType.GetProperty(property.Name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
					if (propertyObj == null)
						throw new SettingsException(Exceptions.PropertyDefinedInSchemaNotFound,
							property.Name, schema.SchemaType.FullName, objType.FullName);
					List<object> attributes = new List<object>();
					attributes.AddRange(property.GetCustomAttributes(false));
					attributes.AddRange(propertyObj.GetCustomAttributes(false));
					AddProperty(propertyObj, attributes.ToArray());
				}
			}
		}
	}
}
